package com.nce.converter.audio;

import java.io.FileNotFoundException;
//import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashMap;

import javax.media.Buffer;
import javax.media.format.AudioFormat;

import com.nce.converter.FileWriter;
import com.nce.converter.Transcoder;
import com.nce.converter.TranscoderOutput;
import com.sun.media.format.WavAudioFormat;

public class WaveFileWriter extends Thread  implements FileWriter{
		private TranscoderOutput[] codecs = null;
		private long [] offsets = null;
		private String strFileName = null;
		private RandomAccessFile outputFile = null;
		private boolean inited = false;
		
		private WavAudioFormat outputFormat = null;
		
		
		private int                    fileSize = 0;
		private int                    filePointer = 0;
//		private long                   fileSizeLimit = -1;
	    
		private int maxBufSize = 32768; // Don't reduce this value below 2048
		private byte [] buf = new byte[maxBufSize];
		private int     bufOffset;
		private int     bufLength;
	    
	    private byte[][] inputBuffers = null;
	    private int[] nextBlockPointer = null;
	    
	    private final static byte [] EMPTY_BLOCK = { 0, 0 };
	    //private int currentActiveChannels = 0;
	    
	    private int blockAlign = 0;
	    private int bytesPerSecond = 0;
	    private int dataSizeOffset;
	    private int numberOfSamplesOffset;
	    private int factChunkLength = 0;
	    private short wFormatTag;
		
		
		
		
		public boolean init(HashMap<String, Object> configInfo)
		{
			int channels = 0;
			long samplerate = 0;
			int samplesizeinbits = 0;
			if (configInfo.containsKey("output.file.name"))
			{
				strFileName = (String) configInfo.get("output.file.name");
			} else 
			{
				inited= false;
				return inited;
			}
			
			if (configInfo.containsKey("input.streams_number") && configInfo.containsKey("input.audio.samplerate"))
			{
				channels = (Integer) configInfo.get("input.streams_number");
				samplerate = (Long) configInfo.get("input.audio.samplerate");
				samplesizeinbits = 16;
			} else 
			{
				inited= false;
				return inited;
			}
			
			inputBuffers = new byte[channels][];
			nextBlockPointer = new int[channels];
			for (int i = 0; i < channels; i++)
			{
				inputBuffers[i]=null;
				nextBlockPointer[i] = 0;
			}
			outputFormat = new WavAudioFormat(AudioFormat.LINEAR, samplerate, samplesizeinbits, channels, 0, 0, null);
			
			inited = true;
			return inited;
		}
		
		
		public void run()
		{
			open();
			writeData();
			close();
		}
		
		
		public void startWriting()
		{
			this.start();
		}
		
		public boolean isWriting()
		{
			return this.isAlive();
		}
		
		public void waitForStop()
		{
			try {
				this.join();
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
		}
		
		public boolean connectToTranscoder(Transcoder t)
		{
			if (t == null)
				return false;
			codecs = t.getOutputs();
			if (codecs == null)
				return false;
			offsets = new long[codecs.length];
			for (int i = 0; i < codecs.length; i++)
			{
				offsets[i] = codecs[i].getOffset();
			}
			return true;
		}
		
		private void retrieveNextInputBuffer(int i)
		{
			if (codecs [i] == null)
			{
				inputBuffers[i] = null;
				nextBlockPointer[i]=-1;
				return;
			}
			Buffer out = codecs[i].getNextOutput();
			
			if ( out == null || out.isEOM())
			{
				inputBuffers[i]=null;
				nextBlockPointer[i]=-1;
				return;
			}
			inputBuffers[i] = (byte[]) out.getData();
			nextBlockPointer[i]=0;
		}
		
		private void fetchInputBuffers()
		{
			for (int i= 0; i< codecs.length; i++)
			{
				retrieveNextInputBuffer(i);
			}
		}
		
		private void writeBlockFromInput(int i)
		{
			if (nextBlockPointer[i] >= inputBuffers[i].length)
			{
				retrieveNextInputBuffer(i);
			}
			if (inputBuffers[i]!=null)
			{
				writeBlock(inputBuffers[i], nextBlockPointer[i]);
				nextBlockPointer[i]+=2;
			}
			else
			{
				writeBlock(EMPTY_BLOCK, 0);
			}
		}
		
		private void writeBlock(byte [] in, int offset)
		{
			if (bufOffset >= maxBufSize-2)
			{
				bufFlush();
				bufClear();
			}
			bufWriteByte(in[offset]);
			bufWriteByte(in[offset+1]);
		}
		
		public void open()
		{
			if (outputFile != null)
				return;
			try {
				outputFile = new RandomAccessFile(strFileName, "rw");
				writeWaveHeader();
			} catch (FileNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		}
		
		public void close()
		{
			writeFooter();
			try {
				outputFile.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		private void write(byte [] data, int offset, int length) {
			if (outputFile == null)
			    return;
			if (length > 0) {
			    filePointer += length;
			    if (filePointer > fileSize)
				fileSize = filePointer;
			   // if (fileSizeLimit > 0 && fileSize >= fileSizeLimit)
				//fileSizeLimitReached = true;
			}
			try {
				outputFile.write(data, offset, length);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
	    }
		
		protected int seek(int location) {
			if (outputFile == null)
			    return location;
			try {
				outputFile.seek(location);
				filePointer = (int) outputFile.getFilePointer();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			return filePointer;
		    }
		
		private void bufClear() {
			bufOffset = 0;
			bufLength = 0;
		}
		
		private void bufFlush() {
			filePointer -= bufLength;  // It is going to be incremented in write()
			write(buf, 0, bufLength);
		}
		
		private void bufWriteBytes(String s) {
			byte[] bytes = s.getBytes();
			bufWriteBytes(bytes);
		}

		private void bufWriteBytes(byte[] bytes) {
			System.arraycopy(bytes, 0, buf, bufOffset, bytes.length);
			bufOffset += bytes.length;
			bufLength += bytes.length;
			filePointer += bytes.length;
		}

		private void bufWriteInt(int value) {
			buf[bufOffset + 0] = (byte) ((value >> 24) & 0xFF);
			buf[bufOffset + 1] = (byte) ((value >> 16) & 0xFF);
			buf[bufOffset + 2] = (byte) ((value >> 8) & 0xFF);
			buf[bufOffset + 3] = (byte) ((value >> 0) & 0xFF);
			bufOffset += 4;
			bufLength += 4;
			filePointer += 4;
		}
		
		private void bufWriteIntLittleEndian(int value) {
			buf[bufOffset + 3] = (byte)((value >>> 24) & 0xFF);
			buf[bufOffset + 2] = (byte)((value >>> 16) & 0xFF);
			buf[bufOffset + 1] = (byte)((value >>>  8) & 0xFF);
			buf[bufOffset + 0] = (byte)((value >>>  0) & 0xFF);
			bufOffset += 4;
			bufLength += 4;
			filePointer += 4;
		 }
		
		private void bufWriteShortLittleEndian(short value) {
			buf[bufOffset + 1] = (byte)((value >> 8) & 0xFF);
			buf[bufOffset + 0] = (byte)((value >> 0) & 0xFF);
			bufOffset += 2;
			bufLength += 2;
			filePointer += 2;
		}
		
		private void bufWriteByte(byte value)
		{
			buf[bufOffset] = value;
			bufOffset += 1;
			bufLength += 1;
			filePointer += 1;
		}
		
		private void writeWaveHeader()
		{
			int formatSize = 16;  // Minimum formatSize
			bufClear();
			bufWriteBytes("RIFF");
			bufWriteInt(0); // This is filled in later when filesize is known
			bufWriteBytes("WAVE");

			bufWriteBytes("fmt "); // Format Chunk
			bufWriteIntLittleEndian(formatSize);
			blockAlign = (outputFormat.getSampleSizeInBits() / 8) * outputFormat.getChannels();
			Integer formatTag =
			    (Integer)
			       WavAudioFormat.reverseFormatMapper.get(outputFormat.getEncoding().toLowerCase());
			wFormatTag = formatTag.shortValue();
			bufWriteShortLittleEndian(wFormatTag); // encoding

			bufWriteShortLittleEndian((short) outputFormat.getChannels());

			bufWriteIntLittleEndian((int) outputFormat.getSampleRate());
			
			//int frameRate = Format.NOT_SPECIFIED;
			
			bytesPerSecond = outputFormat.getChannels() * outputFormat.getSampleSizeInBits() * (int) outputFormat.getSampleRate() / 8;
			
			bufWriteIntLittleEndian(bytesPerSecond);

			bufWriteShortLittleEndian((short) blockAlign);
			
			bufWriteShortLittleEndian((short) outputFormat.getSampleSizeInBits());
			
			factChunkLength = 0;
			if (wFormatTag != WavAudioFormat.WAVE_FORMAT_PCM) {
			    // Compressed formats require a FACT chunk
			    bufWriteBytes("fact"); // Fact Chunk
			    bufWriteIntLittleEndian(4);
			    numberOfSamplesOffset = filePointer;
			    bufWriteInt(0);   // This is filled in later when numberOfSamples is known
			    factChunkLength = 12;
			}

			bufWriteBytes("data"); // Data Chunk
			dataSizeOffset = (int) filePointer;
			bufWriteInt(0);        // This is filled in later when datasize is known
			/*
			fileSize = 4 + // for RIFF
			           4 + // file length
			           4 + // for WAVE

			           4 + // 'fmt ' chunk id
			           4 + //  'fmt ' chunk size field
		                   formatSize +
			           4 + // 'data' chunk id
			           4; //  'data' chunk size field
			*/
			bufFlush();

			
		}
		
		private void writeFooter() {
			// Write the file size
			seek(4);
			bufClear();
			bufWriteIntLittleEndian(fileSize - 8); // -8 since we should skip the first
			                                       // two ints
			bufFlush();
			// Write the data size
			seek(dataSizeOffset);
			bufClear();
			int dataSize = fileSize - (dataSizeOffset + 4);
			bufWriteIntLittleEndian(dataSize);
			bufFlush();
			if (factChunkLength > 0) {
			    // Has a Fact chunk
			    // Calculate numberOfSamples
			    // bytesPerSecond won't be zero
			    float duration = (float) dataSize / bytesPerSecond;
			    int numberOfSamples = (int) (duration * outputFormat.getSampleRate());
			    seek(numberOfSamplesOffset);
			    bufClear();
			    bufWriteIntLittleEndian(numberOfSamples);
			    bufFlush();
			}
		}
		
		private void writeData()
		{
			fetchInputBuffers();
			boolean isAtLeastOneAlive = false;
			do {
				isAtLeastOneAlive = false;
				for (int i = 0; i < codecs.length; i++)
				{
					if (offsets[i]==0 && nextBlockPointer[i] >= 0)
					{
						isAtLeastOneAlive = true;
						writeBlockFromInput(i);
					}
					else
					{
						if (offsets[i]>0)
							offsets[i]--;
						writeBlock(EMPTY_BLOCK, 0);
					}
				}
			} while (isAtLeastOneAlive);
			writeFooter();
		}
		
		
}
